<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
import throttle from "lodash/throttle"

export default {
  name: 'el-table-virtual-scroll',
  props: {
    // 数据源
    data: {
      type: Array,
      required: true
    },
    // 每一个item的默认高度
    height: {
      type: Number,
      default: 60
    },
    // 可视区域高度
    buffer: {
      type: Number,
      default: 500
    },
    // 唯一key 例如 id
    keyProp: {
      type: String,
      default: 'id'
    },
    // 滚动事件延迟监听时间
    throttleTime: {
      type: Number,
      default: 100
    }
  },

  computed: {
    // 计算出每个item（的key值）到滚动容器顶部的距离(item 的索引 * item的高度)   下面采用累加计算的方式
    offsetMap({ keyProp, height, sizes, data }) {
      const res = {}
      let total = 0
      for (let i = 0; i < data.length; i++) {
        const key = data[i][keyProp]
        res[key] = total
        const curSize = sizes[key]
        const size = typeof curSize === 'number' ? curSize : height
        total += size
      }

      return res
    }
  },
  watch: {
    data() {
      this.update()
    }
  },
  created() {
    // 也可放到 mount 中去初始化
    this.$nextTick(() => {
      this.initData()

    })
  },
  beforeDestroy() {
    // 页面销毁前 卸载监听事件
    if (this.scroller) {
      this.scroller.removeEventListener('scroll', this.onScroll)
      window.removeEventListener('resize', this.onScroll)
    }
  },
  data() {
    return {
      sizes: {} // 每一个item的 尺寸映射（依赖响应式）
    }
  },
  methods: {
    // 初始化数据
    initData() {
      // 可视范围内显示数据
      this.renderData = []
      // 页面可视范围顶端、底部
      this.top = undefined
      this.bottom = undefined
      // 截取页面可视范围内显示数据的开始和结尾索引
      this.start = 0
      this.end = undefined
      // 获取表格的body部分 Dom 对象
      this.scroller = this.$el.querySelector('.el-table__body-wrapper')
      // 初次执行
      setTimeout(() => {
        this.handleScroll()
      }, 100)

      // 定义一个 滚动时触发的回调事件(防抖处理)
      this.onScroll = throttle(this.handleScroll, this.throttleTime)
      // 表格的body Dom对象 添加滚动事件
      this.scroller.addEventListener('scroll', this.handleScroll)
      // 当窗口尺寸发生变化 触发 onScroll 回调事件 重新计算值
      window.addEventListener('resize', this.onScroll)
    },

    // 更新尺寸（高度）
    updateSizes() {
      const rows = this.$el.querySelectorAll('.el-table__body > tbody > .el-table__row')
      // 记录每一行的高度 形成{ id : height } 的结构
      Array.from(rows).forEach((row, index) => {
        const item = this.renderData[index]
        if (!item) return
        const key = item[this.keyProp]
        // 获取每一行的高度
        const offsetHeight = row.offsetHeight
        // 如果当前的行 没有被存储过 就进行存储
        if (this.sizes[key] !== offsetHeight) {
          this.$set(this.sizes, key, offsetHeight)
        }

      })
    },

    // 处理滚动事件
    handleScroll(shouldUpdate = true) {

      // 更新当前尺寸（高度）
      this.updateSizes()
      // 计算renderData
      this.calcRenderData()
      // 计算位置
      this.calcPosition()
      shouldUpdate && this.updatePosition()
      // 触发事件
      this.$emit('change', this.renderData, this.start, this.end)
    },

    // 获取某条数据offsetTop 直接从 offsetMap 中根据index获取距离顶部的高度
    getOffsetTop(index) {
      const item = this.data[index]
      if (item) {
        return this.offsetMap[item[this.keyProp]] || 0
      }
      return 0
    },

    // 获取某条数据的尺寸 直接从 size 中根据index获取距离顶部的高度
    getSize(index) {
      const item = this.data[index]
      if (item) {
        const key = item[this.keyProp]
        return this.sizes[key] || this.height
      }
      return this.height
    },

    // 计算只在视图上渲染的数据
    calcRenderData() {
      const { scroller, data, buffer } = this
      // 计算可视范围顶部、底部
      const top = scroller.scrollTop - buffer
      const bottom = scroller.scrollTop + scroller.offsetHeight + buffer

      // 二分法计算可视范围内的开始的第一个内容（计算start节点也就是mid节点）
      let l = 0
      let r = data.length - 1
      let mid = 0
      while (l <= r) {
        mid = Math.floor((l + r) / 2)
        const midVal = this.getOffsetTop(mid)
        if (midVal < top) {
          const midNextVal = this.getOffsetTop(mid + 1)
          if (midNextVal > top) break
          l = mid + 1
        } else {
          r = mid - 1
        }
      }

      // 计算渲染内容的开始、结束索引
      let start = mid
      let end = data.length - 1
      // 从开是的节点向后查找 找到第一个位置 >= bottom 的节点就是 end
      for (let i = start + 1; i < data.length; i++) {
        const offsetTop = this.getOffsetTop(i)
        if (offsetTop >= bottom) {
          end = i
          break
        }
      }

      // 开始索引始终保持偶数，如果为奇数，则加1使其保持偶数【确保表格行的偶数数一致，不会导致斑马纹乱序显示】
      if (start % 2) {
        start = start - 1
      }
      // console.log(start, end, 'start end')

      this.top = top
      this.bottom = bottom
      this.start = start
      this.end = end
      this.renderData = data.slice(start, end + 1)
    },

    // 计算位置
    calcPosition() {
      const last = this.data.length - 1
      // 计算内容总高度 最后一个节点距离顶部的位置 + 本身节点的高度
      const wrapHeight = this.getOffsetTop(last) + this.getSize(last)
      // 计算当前滚动位置需要撑起的高度
      const offsetTop = this.getOffsetTop(this.start)

      // 设置dom位置
      const classNames = ['.el-table__body-wrapper', '.el-table__fixed-right .el-table__fixed-body-wrapper', '.el-table__fixed .el-table__fixed-body-wrapper']
      classNames.forEach(className => {
        const el = this.$el.querySelector(className)
        if (!el) return

        // 创建wrapEl、innerEl
        if (!el.wrapEl) {
          const wrapEl = document.createElement('div')
          const innerEl = document.createElement('div')
          wrapEl.appendChild(innerEl)
          innerEl.appendChild(el.children[0])
          el.insertBefore(wrapEl, el.firstChild)
          el.wrapEl = wrapEl
          el.innerEl = innerEl
        }

        if (el.wrapEl) {
          // 设置高度
          el.wrapEl.style.height = wrapHeight + 'px'
          // 设置transform撑起高度
          el.innerEl.style.transform = `translateY(${offsetTop}px)`
          // 设置paddingTop撑起高度
          // el.innerEl.style.paddingTop = `${offsetTop}px`
        }
      })
    },

    // 空闲时更新位置
    updatePosition() {
      this.timer && clearTimeout(this.timer)
      this.timer = setTimeout(() => {
        this.timer && clearTimeout(this.timer)
        // 传入false，避免一直循环调用
        this.handleScroll(false)
      }, this.throttleTime + 10)
    },

    // 【外部调用】更新
    update() {
      console.log('update')
      this.handleScroll()
    },

    // 【外部调用】滚动到第几行
    scrollTo(index, stop = false) {
      const item = this.data[index]
      if (item && this.scroller) {
        this.updateSizes()
        this.calcRenderData()

        this.$nextTick(() => {
          const offsetTop = this.getOffsetTop(index)
          this.scroller.scrollTop = offsetTop

          // 调用两次scrollTo，第一次滚动时，如果表格行初次渲染高度发生变化时，会导致滚动位置有偏差，此时需要第二次执行滚动，确保滚动位置无误
          if (!stop) {
            setTimeout(() => {
              this.scrollTo(index, true)
            }, 50)
          }
        })
      }
    },

    // 【外部调用】重置
    reset() {
      this.sizes = {}
      this.scrollTo(0, false)
    }
  },

}
</script>

<style lang='less' scoped>

</style>
